﻿Imports System.Reflection

''' <summary>
''' 列属性
''' </summary>
''' <remarks></remarks>
<AttributeUsage(AttributeTargets.Property)>
Public Class ColumnAttribute
    Inherits Attribute

    Private Shared _onColumnNameChanging As Func(Of String, String)
    ''' <summary>
    ''' 列名変更イベント
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' #101 生成されるSQL文にエスケープ文字を追加する
    ''' </remarks>
    Public Shared Property OnColumnNameChanging As Func(Of String, String)
        Get
            If _onColumnNameChanging Is Nothing Then
                _onColumnNameChanging = Function(s) s
            End If
            Return _onColumnNameChanging
        End Get
        Set(value As Func(Of String, String))
            _onColumnNameChanging = value
        End Set
    End Property

#Region "Constructor"
    Public Sub New()
    End Sub

    Public Sub New(ByVal ColumnName As String)
        Me.ColumnName = ColumnName
    End Sub

    ''' <summary>
    ''' コンストラクタ
    ''' </summary>
    ''' <param name="yourColumn"></param>
    ''' <remarks>
    ''' #101 生成されるSQL文にエスケープ文字を追加する
    ''' </remarks>
    Friend Sub New(yourColumn As ColumnAttribute)
        '再度文字加工が起きないようメンバ変数へ代入
        _columnName = yourColumn.ColumnName
        _plainColumnName = yourColumn.PlainColumnName
        Me.PropertyInfo = yourColumn.PropertyInfo
    End Sub
#End Region

#Region "BasicInfo"
    ''' <summary>
    ''' テーブル属性
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Table As TableAttribute

    Friend _columnName As String
    ''' <summary>
    ''' 列名
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property ColumnName As String
        Get
            Return _columnName
        End Get
        Set(value As String)
            '#101 生成されるSQL文にエスケープ文字を追加する
            _plainColumnName = value
            _columnName = ColumnAttribute.OnColumnNameChanging.Invoke(value)
        End Set
    End Property

    Private _plainColumnName As String
    ''' <summary>
    ''' 列名（未エスケープ処理）
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' #101 生成されるSQL文にエスケープ文字を追加する
    ''' </remarks>
    Public ReadOnly Property PlainColumnName As String
        Get
            Return _plainColumnName
        End Get
    End Property

    ''' <summary>
    ''' 主キーであるかどうかを示します
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property IsPrimaryKey As Boolean
        Get
            Return If(Me.Key IsNot Nothing, True, False)
        End Get
    End Property

    Private _key As KeyAttribute
    ''' <summary>
    ''' 主キー属性
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property Key() As KeyAttribute
        Get
            Return _key
        End Get
        Friend Set(ByVal value As KeyAttribute)
            _key = value
            If _key IsNot Nothing Then Key.Column = Me
        End Set
    End Property

    ''' <summary>
    ''' 親関連属性かどうかを示します
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable ReadOnly Property IsParent As Boolean
        Get
            Return False
        End Get
    End Property
#End Region

#Region "GetValue SetValue"
    ''' <summary>
    ''' プロパティ
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property PropertyInfo As PropertyInfo

    ''' <summary>
    ''' プロパティの型を取得します
    ''' Nullableの場合、格納できる型が返されます
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Friend Function GetPropertyType() As Type
        Return PropertyReader.GetActualPropertyType(Me.PropertyInfo)
    End Function

    ''' <summary>
    ''' 値を代入します
    ''' </summary>
    ''' <param name="model"></param>
    ''' <param name="val"></param>
    ''' <remarks></remarks>
    Public Overridable Sub SetValue(model As Object, val As Object)
        PropertyWriter.SetValue(Me.PropertyInfo, model, val)
    End Sub

    ''' <summary>
    ''' 値を取得します
    ''' </summary>
    ''' <param name="model"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable Function GetValue(model As Object) As Object
        Return Me.PropertyInfo.GetValue(model, Nothing)
    End Function
#End Region

#Region "SQL Load Property"

#Region "Select"
    Public Property SelectCreater As Action(Of Object, Action(Of String), String, String) = AddressOf Me.DefaultSelectCreater

    Private Sub DefaultSelectCreater(instance As Object, sqlColumnBuilder As Action(Of String), tableAlias As String, columnAlias As String)
        If Me.IsLoadable = False Then Return
        sqlColumnBuilder.Invoke(String.Format("{0}.{1} AS {2}", tableAlias, Me.ColumnName, columnAlias))
    End Sub

    Private _IsLoadable As Boolean = True
    ''' <summary>
    ''' 読み込みできるかどうかを示します。
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' </remarks>
    Public Property IsLoadable As Boolean
        Get
            If _IsLoadable = False Then Return False
            If Me.SelectCreater Is Nothing Then Return False
            Return True
        End Get
        Set(value As Boolean)
            _IsLoadable = value
        End Set
    End Property

    Public Overridable Sub BuildSelectColumnCommand(instance As Object, sqlColumnBuilder As Action(Of String), tableAlias As String, columnAlias As String)
        If Me.IsLoadable = False Then Return
        sqlColumnBuilder.Invoke(String.Format("{0}.{1} AS {2}", tableAlias, Me.ColumnName, columnAlias))
    End Sub
#End Region

#Region "Where of Load"
    Public Property LoadWhereCommandBuilder As Action(Of Object, Action(Of String), Func(Of Object, String)) = Nothing

    Public Sub BuildLoadWhereCommand(instance As Object, sqlBuilder As Action(Of String), paramRegister As Func(Of Object, String))
        If Me.KeyLoadWhereCommandBuilder Is Nothing Then Return
        Me.LoadWhereCommandBuilder.Invoke(instance, sqlBuilder, paramRegister)
    End Sub
#End Region

#Region "Where of LoadKey"
    Public Property KeyLoadWhereCommandBuilder As Action(Of Object, Action(Of String), Func(Of Object, String)) = Nothing

    Public ReadOnly Property IsLoadKey As Boolean
        Get
            Return If(Me.KeyLoadWhereCommandBuilder Is Nothing, False, True)
        End Get
    End Property

    Public Sub BuildKeyLoadWhereCommand(instance As Object, sqlBuilder As Action(Of String), paramRegister As Func(Of Object, String))
        If Me.KeyLoadWhereCommandBuilder Is Nothing Then Return
        Me.KeyLoadWhereCommandBuilder.Invoke(instance, sqlBuilder, paramRegister)
    End Sub
#End Region

#Region "Where of AlternativeKeyLoad"
    Public Property AlternativeKeyLoadWhereCommandBuilder As Action(Of Object, Action(Of String), Func(Of Object, String)) = Nothing

    Public ReadOnly Property IsAlternativeLoadKey As Boolean
        Get
            Return If(Me.AlternativeKeyLoadWhereCommandBuilder Is Nothing, False, True)
        End Get
    End Property

    Public Sub BuildAlternativeKeyLoadWhereCommand(instance As Object, sqlBuilder As Action(Of String), paramRegister As Func(Of Object, String))
        If Me.AlternativeKeyLoadWhereCommandBuilder Is Nothing Then Return
        Me.AlternativeKeyLoadWhereCommandBuilder.Invoke(instance, sqlBuilder, paramRegister)
    End Sub
#End Region

#Region "Where of Reload"
    Public Property ReLoadWhereCommandCreater As Action(Of Object, Action(Of String), Func(Of Object, String)) = Nothing

    Public ReadOnly Property IsRelaodKey As Boolean
        Get
            Return If(Me.ReLoadWhereCommandCreater Is Nothing, False, True)
        End Get
    End Property

    Public Sub BuildReLoadWhereCommand(instance As Object, sqlBuilder As Action(Of String), paramRegister As Func(Of Object, String))
        If Me.ReLoadWhereCommandCreater Is Nothing Then Return
        Me.ReLoadWhereCommandCreater.Invoke(instance, sqlBuilder, paramRegister)
    End Sub
#End Region

#End Region

#Region "SQL Save Property"

    ''' <summary>
    ''' Nullable型かどうかを判定します。
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overridable ReadOnly Property IsNullable() As Boolean
        Get
            Return PropertyReader.IsNullable(Me.PropertyInfo)
        End Get
    End Property

#Region "Dirty Check"
    Public Property DirtyChecker As Func(Of Object, Object, Boolean) = AddressOf Me.DefaultDirtyChecker

    Private Function DefaultDirtyChecker(actualValue As Object, archiveValue As Object) As Boolean
        Return Not Comparer.Equals(actualValue, archiveValue)
    End Function

    ''' <summary>
    ''' 現在値とアーカイブ値を比較します。
    ''' </summary>
    ''' <param name="instance">インスタンス（プロパティ値ではないので注意）</param>
    ''' <param name="archiveValue">アーカイブ値</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function IsDirty(instance As Object, archiveValue As Object) As Boolean
        If Me.DirtyChecker Is Nothing Then Throw New InvalidOperationException("DirtyCheckerが登録されていません。")
        Return Me.DirtyChecker.Invoke(Me.GetValue(instance), archiveValue)
    End Function
#End Region

#Region "Insert"
    Private _IsInsertable As Boolean = True
    ''' <summary>
    ''' 追加ができるかどうかを示します。
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' </remarks>
    Public Property IsInsertable As Boolean
        Get
            If _IsInsertable = False Then Return False
            If Me.InsertColumnCommandCreater Is Nothing Then Return False
            Return True
        End Get
        Set(value As Boolean)
            _IsInsertable = value
        End Set
    End Property

    Public Property InsertColumnCommandCreater As Action(Of Object, Action(Of String), Action(Of String), Func(Of Object, Type, String)) = AddressOf DefaultInsertColumnCreater

    Friend Sub DefaultInsertColumnCreater(instance As Object, sqlColumnBuilder As Action(Of String), sqlValueBuilder As Action(Of String), paramRegister As Func(Of Object, Type, String))
        Dim actual As Object = Me.GetValue(instance)
        If actual Is Nothing AndAlso Me.GetPropertyType.Equals(GetType(String)) Then actual = String.Empty

        Dim pname As String = paramRegister.Invoke(actual, Me.PropertyInfo.PropertyType)
        sqlColumnBuilder.Invoke(Me.ColumnName)
        sqlValueBuilder.Invoke(pname)
    End Sub

    ''' <summary>
    ''' 追加用コマンドを生成します。
    ''' </summary>
    ''' <param name="instance">インスタンス</param>
    ''' <param name="sqlColumnBuilder">SQLビルダー。代入値を渡してください。パラメータクエリを使用したい場合は、レジスタを使用してパラメータ名を渡します。</param>
    ''' <param name="paramRegister">パラメータレジスター。代入値を渡してください。パラメータ名が返されます。</param>
    ''' <remarks>
    ''' 保存コマンドを組み立てます。
    ''' </remarks>
    Public Sub BuildInsertColumnCommand(instance As Object, sqlColumnBuilder As Action(Of String), sqlValueBuilder As Action(Of String), paramRegister As Func(Of Object, Type, String))
        If Me.IsInsertable = False Then Return
        Me.InsertColumnCommandCreater.Invoke(instance, sqlColumnBuilder, sqlValueBuilder, paramRegister)
    End Sub
#End Region

#Region "Update"
    Private _isUpdatable As Boolean = True
    ''' <summary>
    ''' 更新できるかどうかを示します。
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Property IsUpdatable() As Boolean
        Get
            If Me._isUpdatable = False Then Return False
            If Me.UpdateColumnCommandCreater Is Nothing Then Return False
            Return True
        End Get
        Set(ByVal value As Boolean)
            _isUpdatable = value
        End Set
    End Property

    Public Property UpdateColumnCommandCreater As Action(Of Object, Object, Action(Of String), Func(Of Object, Type, String)) = AddressOf DefaultUpdateColumnCreater

    Friend Sub DefaultUpdateColumnCreater(instance As Object, archiveValue As Object, sqlBuilder As Action(Of String), paramRegister As Func(Of Object, Type, String))
        Dim actual As Object = Me.GetValue(instance)
        If Me.DirtyChecker Is Nothing Then Throw New InvalidOperationException("DirtyCheckerが登録されていません。")
        If Me.DirtyChecker.Invoke(actual, archiveValue) = False Then Return

        Dim pname As String = paramRegister.Invoke(actual, Me.PropertyInfo.PropertyType)
        sqlBuilder.Invoke(String.Format("{0} = {1}", Me.ColumnName, pname))
    End Sub

    ''' <summary>
    ''' 更新用コマンドを生成します。
    ''' </summary>
    ''' <param name="instance">インスタンス</param>
    ''' <param name="archiveValue">アーカイブ値。</param>
    ''' <param name="sqlBuilder">SQLビルダー。代入値を渡してください。パラメータクエリを使用したい場合は、レジスタを使用してパラメータ名を渡します。</param>
    ''' <param name="paramRegister">パラメータレジスター。代入値を渡してください。パラメータ名が返されます。</param>
    ''' <remarks>
    ''' 保存コマンドを組み立てます。
    ''' ダーティチェックの結果にかかわらずコールされますので、再度ダーティチェックを行う必要があります。
    ''' </remarks>
    Public Sub BuildUpdateColumnCommand(instance As Object, archiveValue As Object, sqlBuilder As Action(Of String), paramRegister As Func(Of Object, Type, String))
        If Me.IsUpdatable = False Then Return
        Me.UpdateColumnCommandCreater.Invoke(instance, archiveValue, sqlBuilder, paramRegister)
    End Sub
#End Region

#Region "Where Of Save"
    Public Property SaveWhereCommandCreater As Action(Of Object, Object, Action(Of String), Func(Of Object, String)) = Nothing

    Public ReadOnly Property IsSaveKey As Boolean
        Get
            Return If(Me.SaveWhereCommandCreater Is Nothing, False, True)
        End Get
    End Property

    Public Sub BuildSaveWhereCommand(instance As Object, archiveValue As Object, sqlBuilder As Action(Of String), paramRegister As Func(Of Object, String))
        If Me.IsSaveKey = False Then Return
        Me.SaveWhereCommandCreater.Invoke(instance, archiveValue, sqlBuilder, paramRegister)
    End Sub
#End Region

#End Region

    Public Overrides Function ToString() As String
        Dim colName As String = String.Format("{0}.{1}", If(Me.Table Is Nothing, "unknown", Me.Table.TableName), Me.ColumnName)
        Dim propName As String = If(Me.PropertyInfo Is Nothing, "unknown", String.Format("{0}.{1}", Me.PropertyInfo.DeclaringType.Name, Me.PropertyInfo.Name))

        Dim keyName As String = If(Me.IsLoadKey, "*", String.Empty)

        Return String.Format("{0}{1} ({2})", keyName, colName, propName)
    End Function

    Public Overridable Function ColumnEquals(col As ColumnAttribute) As Boolean
        If col Is Nothing Then Return False
        If col.Table Is Nothing OrElse Me.Table Is Nothing OrElse col.Table.TableFullName <> Me.Table.TableFullName Then Return False
        If col.ColumnName <> Me.ColumnName Then Return False

        Return True
    End Function

    ''' <summary>
    ''' リレーションルート列属性を取得します
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>
    ''' #103 親－子－孫の関係時、子、孫を起点とした読み込みができない
    ''' </remarks>
    Public Overridable Function GetRelationRootColumn() As ColumnAttribute
        'リレーションではないので、自身がルートです
        Return Me
    End Function

End Class
